package edu.colorado.karl.intelsched;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Calendar;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import edu.colorado.karl.databases.ErrorDatabaseResult;


//NOTE: Current forecast + next (ie, run afternoon get tonight, run tonight get tomorrow)
/**
 *  The RSSReader fetches and reads the current RSS feed from a given weather
 *  RSS URL.  The retrieval of the RSS feed is done by invoking an external 
 *  python script, which is then parsed to find the following information:
 * <ul>
 * <li>High and low temperatures
 * <li>Forecast conditions: sunny, mostly cloudy, partly cloudy, windy, snow 
 * showers, rain, fog, isolated snow/rain showers, and wind speed
 * <li>Chance of precipitation (or of snow)
 * </ul>
 *  The main (and only) method to invoke is getPredictedWeather with an 
 *  optional URL for an RSS.  If no URL is provided, the reader will use a 
 *  default URL pointing to rssweather.com
 * @author Michael Knudson
 * @version 4/14/08
 * @see	#getPredictedWeather()
 * @see #getPredictedWeather(String)
 * @see Weather
 */
public class RSSReader {
	private String URL = "http://www.rssweather.com/wx/us/co/boulder/rss.php";
	//used for log4j logging
	private static final Log log = LogFactory.getLog(RSSReader.class);
	
	/**
	 * Creates a new instance of a RSSReader.
	 */
	public RSSReader() {
		
	}
	
	//	Strips off any non-numeric characters from the end of a number
	private int extractNumber(String s) {
		char[] c = s.toCharArray();
		int endIndex = 0;
		for(int i = c.length-1; i > 0; --i) {
			if(isNumber(c[i])) {
				endIndex = i;
				break;
			}
		}
		return Integer.parseInt(s.substring(0, endIndex+1));
	}
	
	/**
	 * Retrieves the current RSS feed from the provided RSS URL.
	 */
	private RSSData getCurrentFeed(String url) {
		String command = "python readrss.py " + url;
		Runtime r = Runtime.getRuntime();
		RSSData data = null;

		try {
			//	Execute the Python script
//			Process p = r.exec(command, null, new File("src/edu/colorado/karl/intelsched"));
			Process p = r.exec(command, null, new File("WEB-INF/classes/edu/colorado/karl/intelsched"));	// use on the server
			
			//	Fetch and store the script's output
			BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
			
			data = parseRSS(br);
			
			if(p.waitFor() != 0) {
			}
		} catch (Exception e) {
			log.warn("Reader unable to reach RSS feed: " + url);
			ErrorDatabaseResult.activateError(ErrorDatabaseResult.REACH_RSS);
		}
		
		return data;
	}

	private void getForecast(String desc, Weather w) {
		if(desc.contains("sunny"))
			w.setSunny(true);
		if(desc.contains("isolated")) {
			if(desc.contains("isolated snow showers"))
				w.setIsolatedSnow(true);
			if(desc.contains("isolated rain showers"))
				w.setIsolatedShowers(true);
		} else {
			if(desc.contains("snow"))
				w.setSnowy(true);
			if(desc.contains("rain"))
				w.setRainy(true);
		}
		if(desc.contains("wind"))
			w.setWindy(true);
		if(desc.contains("mostly cloudy"))
			w.setMostlyCloudy(true);
		if(desc.contains("partly cloudy"))
			w.setPartlyCloudy(true);
		if(desc.contains("fog"))
			w.setFoggy(true);
		
		if(w.isSnowy() && w.getPrecipitationChance() > 0) {
			w.setSnowChance(w.getPrecipitationChance());
			w.setPrecip(0);
		}
	}
	
	/**
	 * Fetches and reads a weather RSS feed to store data about predicted 
	 * weather conditions for the near future.  If the reader is run during 
	 * the AM hours, it will attempt to read weather conditions for that 
	 * evening.  If the reader is run during the PM hours, it will attempt to
	 * read weather conditions for the following day.  The weather information
	 * is stored and returned as a {@link Weather} object.
	 * <p> This version of the function uses the built-in default URL:
	 * <code>http://www.rssweather.com/wx/us/co/boulder/rss.php</code> .  To 
	 * specify a particular RSS URL to use, use the overloaded version of this
	 * function {@link #getPredictedWeather(String)}.
	 * @return	A weather object containing temperature, forecast, and 
	 * estimated precipitation data.
	 * @see #getPredictedWeather(String)
	 * @see Weather
	 */
	public Weather getPredictedWeather() {
		return getPredictedWeather(URL);
	}

	/**
	 * Fetches and reads the provided weather RSS feed to store data about 
	 * predicted weather conditions for the near future.  If the reader is 
	 * run during the AM hours, it will attempt to read weather conditions for
	 * that evening.  If the reader is run during the PM hours, it will attempt
	 * to read weather conditions for the following day.  The weather
	 * information is stored and returned as a {@link Weather} object.
	 * <p> This version of the function uses the URL specified by 
	 * <code>url</code>.  To use the built-in URL, use the version of this 
	 * function with no parameters: {@link #getPredictedWeather()}.
	 * @param url	The URL of a weather RSS feed.
	 * @return	A weather object containing temperature, forecast, and 
	 * estimated precipitation data.
	 * @see #getPredictedWeather()
	 * @see Weather
	 */
	public Weather getPredictedWeather(String url) {
		if(url == null)
			url = URL;
//		String url2 = "http://rss.wunderground.com/auto/rss_full/CO/Boulder.xml";
		RSSData data = getCurrentFeed(url);

		if(data == null || data.size() == 0) {
			log.warn("Reader unable to reach RSS feed: " + url);
			ErrorDatabaseResult.activateError(ErrorDatabaseResult.REACH_RSS);
			return null;
		}
		
		//		data.printAll();	// Debugging display
		
		//	Determine if this is a morning check or afternoon check
		Calendar cal = Calendar.getInstance();
		boolean morning = false;
		if(cal.get(Calendar.AM_PM) == Calendar.AM)
			morning = true;
		
		morning = true;
		//	Find the appropriate article for the current time
		int index = -1;
		if(morning) {
			for(int i=0; i<data.size(); ++i) {
				if(data.getTitle(i).contains("night")) {
					index = i;
					break;
				}
			}
		} else {
			String day = getTomorrow();	//	The name of tomorrow (eg, "sunday" or "monday")
			
			//	Find the article with tomorrow's day
			for(int i=0; i<data.size(); ++i) {
				if(data.getTitle(i).contains(day)) {
					index = i;
					break;
				}
			}
		}
		
		if(index == -1) {
			ErrorDatabaseResult.activateError(ErrorDatabaseResult.PARSE_RSS);
			log.warn("Reader unable to find appropriate article.");
			return null;
		}
//		System.out.println("Want: [" + index + "]");
		
		Weather w = new Weather();

		//	Get the highs and lows
		getTemp(data, index, w);
		
		//	Parse out the forecast
		getForecast(data.getDesc(index), w);
		
		if(w.getHighTemperature() == -666 || w.getLowTemperature() == -666) {
			ErrorDatabaseResult.activateError(ErrorDatabaseResult.PARSE_RSS);
			log.warn("Reader unable to parse both temperatures.");
		}
		
		return w;
	}

	private void getTemp(RSSData data, int index, Weather w) {
		//	NOTE: The word checks used to ensure articles contain information 
		//	about high temperatures may fail on days with high wind
		boolean high = true;
		if(data.getDesc(index).contains("low"))
			high = false;
		
		//	Parse out the temperature
		getNumbers(data.getDesc(index), w, high);
		
		//	Now we need the other temperature for the range
		//	For any reason, if we are unable to find the other temperature, we just keep going		
		
		//	If we found the high before, we need to find the low - usually the night before
		if(high) {
			//	First, check the same article, some sites list both at once
			if(data.getDesc(index).contains("low"))
				getNumbers(data.getDesc(index), w, false);
			
			//	Else, we're looking at the first article and can't go back in time
			else if(index == 0 && data.size() > 1) {
				//	Try the next night, assuming the next article is the next night
				if(data.getDesc(1).contains("low"))
					getNumbers(data.getDesc(1), w, false);
			} else {
				//	Check the night before
				//	NOTE: We make the assumption that tomorrow's article is either 
				//	preceded by tonight's or followed by tomorrow night's article
				if(data.getDesc(index - 1).contains("low"))
					getNumbers(data.getDesc(index - 1), w, false);
				//	Else, try the next night
				else if(data.size() > (index + 1) && data.getDesc(index + 1).contains("low"))
					getNumbers(data.getDesc(index + 1), w, false);
			}
		}	// In this case, we found the low first, and now we need the high
		else {
			//	First, check the same article, some sites list both at once
			if(data.getDesc(index).contains("high"))
				getNumbers(data.getDesc(index), w, true);

			//	Else, we have tonight's article, we need to either figure out today's high or tomorrow's high
			//	In both cases we assume tonight's article is either preceded by
			//	today's or followed by tomorrow's article
			else if(index == 0 && data.size() > 1) {
				if(data.getDesc(1).contains("high"))
					getNumbers(data.getDesc(1), w, true);
			} else {
				if(data.getDesc(index - 1).contains("high"))
					getNumbers(data.getDesc(index - 1), w, true);
				//	Else, try the next day
				else if(data.size() > (index + 1) && data.getDesc(index + 1).contains("high"))
					getNumbers(data.getDesc(index + 1), w, true);
			}
		}
	}

	private void getNumbers(String desc, Weather w, boolean high) {
		//	Array of words in the description
		String[] s = desc.split(" ");
		// List of numbers found
		ArrayList<Integer> nums = new ArrayList<Integer>();
		// List of words following a number
		ArrayList<String> nextword = new ArrayList<String>();
		// List of words preceding a number
		ArrayList<String> prevword = new ArrayList<String>();
								/*	nums[i] is preceded by prevword[i] and followed
								 * by nextword[i].  If there is no previous or next
								 * word, "" is substituted instead.
								 */
		String last = "";		 //	The last word found, used to find value for prevword
		boolean lastNum = false; //	If the last word read was a number
		
		//	Extract all numbers
		try {
			for(int i=0; i < s.length; ++i) {
				if(s[i].equals("")) continue;
				
				if(lastNum) {
					if(isNumber(s[i].charAt(0))) {
						nextword.add("");
						prevword.add(last);
					} else {
						nextword.add(s[i]);
						prevword.add(last);
						lastNum = false;
					}
				}
				
				//	We assume that all numbers are preceded only by white space
				if(!isNumber(s[i].charAt(0))) {
					last = s[i];
					continue;
				}
				
				nums.add(extractNumber(s[i]));
				lastNum = true;
			}
			if(lastNum) {
				nextword.add("");
				prevword.add(last);
			}
			
			//	All lists should be the same size
			if(!((nums.size() == nextword.size()) && (nums.size() == prevword.size())))
				throw new RuntimeException("RSSReader: Parsing error");
			
		} catch (NumberFormatException e) {
			return;
		}
		
		if(nums.size() != nextword.size()) {
			return;
		}
		
		//	Separate out the numbers which correspond to wind (mph) 
		//	and precipitation (percent) from temp
		parseNumbers(nums, nextword, prevword, w, high);
	}
	
	private String getTomorrow() {
		Calendar cal = Calendar.getInstance();

		cal.roll(Calendar.DAY_OF_WEEK, true);
		int daynum = cal.get(Calendar.DAY_OF_WEEK);
		switch(daynum) {
		case Calendar.SUNDAY:
			return "sunday";
		case Calendar.MONDAY:
			return "monday";
		case Calendar.TUESDAY:
			return "tuesday";
		case Calendar.WEDNESDAY:
			return "wednesday";
		case Calendar.THURSDAY:
			return "thursday";
		case Calendar.FRIDAY:
			return "friday";
		case Calendar.SATURDAY:
			return "saturday";
		default:
			throw new IllegalArgumentException("Unknown day: " + daynum);
		}
	}

	//	Returns true if c is a number
	private boolean isNumber(char c) {
		if(c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || 
				c == '5' || c == '6' || c == '7' || c == '8' || c == '9')
			return true;
		return false;
	}

	//	Separate out the numbers which correspond to wind (mph) 
	//	and precipitation (percent) from temp
	private void parseNumbers(ArrayList<Integer> nums, 
							ArrayList<String> nextword, 
							ArrayList<String> prevword, 
							Weather w, 
							boolean high) {
		int x = -666;
		boolean inRange = false;
		for(int i=0; i<nextword.size(); ++i) {
//			System.out.println(nums.get(i) + " " + nextword.get(i));
			if(inRange) {
				if(x == -666) {
					// Throw out this range, its invalid, maybe another range
					// in the list holds the temperature, so don't abort yet
					inRange = false;
					continue;
				}
				
				if(nextword.get(i).contains("mph")) {
					// Number entry was for wind, store max
					if(w.getWindSpeed() < nums.get(i))
						w.setWindSpeed(nums.get(i));
					inRange = false;
					x = -666;
					continue;
				}
				
				if(nextword.get(i).contains("percent")) {
					// Number entry was for precipitation, store max
					w.setPrecip(nums.get(i));
					inRange = false;
					x = -666;
					continue;
				}
				
				//	Else, we assume it was for temperature, 
				//	save the extreme end of the range
				if(high) {
					if(nums.get(i) > x)
						x = nums.get(i);
					// If we already found a higher temperature, keep it:
					if(x > w.getHighTemperature())
						w.setHighT(x);
				} else {
					if(nums.get(i) < x)
						x = nums.get(i);
					if(x < w.getLowTemperature() || w.getLowTemperature() == -666)
						w.setLowT(x);
				}
				
				x = -666;
				inRange = false;
				continue;
			}
			
			if(nextword.get(i).contains("to")) {	//	Range indicator
				x = nums.get(i);
				inRange = true;
				continue;
			}
			
			if(nextword.get(i).contains("mph")) {	// Number entry was for wind
				w.setWindSpeed(nums.get(i));
				continue;
			}
			
			if(nextword.get(i).contains("percent")) { // Number entry was for precipitation
				w.setPrecip(nums.get(i));
				continue;
			}
			
			//	Else, we assume the number is temperature
			x = nums.get(i);
			
			if(prevword.get(i).equals("upper") && (x % 10 == 0))
				//	Denotes a range, 60 in "upper 60s"
				x = x + 8;
			if(prevword.get(i).equals("mid") && (x % 10 == 0))
				//	Denotes a range, 60 in "mid 60s"
				x = x + 5;
			
			if(high && x > w.getHighTemperature())
				w.setHighT(x);
			else if(!high && (x < w.getLowTemperature() || w.getLowTemperature() == -666))
				w.setLowT(x);
			
			x = -666;
		}
	}

	private RSSData parseRSS(BufferedReader br) {
		StringArray titles = new StringArray(),descs = new StringArray();
		boolean title = false;
		String s = "";
		try {
			String line;

			while ((line = br.readLine()) != null) {
				if(line.contains("title:")) {
					if(title)
						throw new IllegalArgumentException("Found consecutive article titles on: " + line);
					titles.add(line.substring(7).toLowerCase());
					title = true;
				}
				else if(!line.equals("")) {
					if(!title)
						throw new IllegalArgumentException("Missing Article Title for: " + line);
					if(line.contains("description:"))
						s = line.substring(13).toLowerCase();
					else
						s = s + " " + line.toLowerCase();
				} else {
					if(s.equals("")) continue;
					descs.add(s);
					title = false;
					s = "";
				}
			}
			// Close the last description, if its still open
			if(title && !s.equals(""))
				descs.add(s);
			
			br.close();
		} catch (Exception e) {
			log.warn("Reader encountered error parsing RSS feed");
			ErrorDatabaseResult.activateError(ErrorDatabaseResult.PARSE_RSS);
		}
		
		return new RSSData(titles, descs);
	}
}

/*	Working RSS Feeds:
 * 	http://www.rssweather.com/wx/us/co/boulder/rss.php
Three versions of feed (Morning, Noon, Evening), all three are readable by this reader.
 *	http://rss.wunderground.com/auto/rss_full/CO/Boulder.xml
Three versions of feed (Morning, Noon, Evening), all three are readable by this reader.
 */